package consul

import (
	"cappy/app/service/conf"
	"context"
	"encoding/base64"
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"github.com/google/uuid"
	pkgerrors "github.com/pkg/errors"
	"io/ioutil"
	"net/http"
	"os"
	"path"
	"strconv"
	"strings"
	"sync"
	"time"
)

type Config struct {
	Host         string
	Port         string
	Env          string
	ConfPath     string
	WatchPrefix  string
	TempConfPath string
}

type consulDriver struct{}

type consulClient struct {
	conf            *Config
	longPoller      *longPoller
	httpCli         *http.Client
	cache           *Cache
	consulIndex     uint64
	lastConsulIndex uint64
	ctx             context.Context
	cancel          context.CancelFunc
	wg              sync.WaitGroup
}

type longPoller struct {
	conf *Config
}

type configEntity struct {
	LockIndex   uint64
	Key         string
	Flags       int64
	Value       string
	CreateIndex uint64
	ModifyIndex uint64
}

var (
	confHost, confPort, confEnv, confPath, tempConfPath, confWatchPrefix string
)
var ErrStatusNotFound = errors.New("not found")
var ErrStatusGatewayTimeout = errors.New("gateway timeout")
var ErrStatusNotOk = errors.New("status not ok")

const (
	_retryInterval        = 1 * time.Second
	_defaultConfApi       = "http://%s:%s/v1/kv/%s?index=%s&recurse=1"
	_consulIndexHeaderKey = "X-Consul-Index"
)

func init() {
	addConsulFlags()
	conf.Resigter(ConfDriverConsul, &consulDriver{})
}

func addConsulFlags() {
	flag.StringVar(&confHost, "consul.host", "consul.bytehello.com", "consul host")
	flag.StringVar(&confPort, "consul.port", "80", "consul port")
	flag.StringVar(&confEnv, "consul.env", "test", "consul env e.g. product")
	flag.StringVar(&confPath, "consul.path", "/data/config", "Config file path")
	flag.StringVar(&tempConfPath, "consul.tempPath", "/data/config/tmp", "temp Config file path")
	flag.StringVar(&confWatchPrefix, "consul.watchPrefix", "data/config", "consul watchPrefix")
}

func (driver *consulDriver) New() (conf.Client, error) {
	var (
		err error
		cf  *Config
	)
	if cf, err = buildConfigForConsul(); err != nil {
		return nil, err
	}
	return driver.new(cf)
}

func (driver *consulDriver) new(conf *Config) (conf.Client, error) {
	var err error
	c := &consulClient{
		conf:    conf,
		httpCli: &http.Client{},
		longPoller: &longPoller{
			conf: conf,
		},
		cache: newCache(),
		wg:    sync.WaitGroup{},
	}
	c.ctx, c.cancel = context.WithCancel(context.Background())
	if err = c.configConfPath(); err != nil {
		return c, err
	}
	return c, err
}

func (c *consulClient) Close() (err error) {
	c.cancel()
	fmt.Println("wait exit")
	c.wg.Wait()
	return nil
}

func (c *consulClient) WatchConf() (err error) {
	c.wg.Add(1)
	go c.start()
	return nil
}

func (c *consulClient) start() {
	c.preload()
	c.initConsulIndex()
	c.watchUpdates()
}

func (c *consulClient) watchUpdates() {
	timer := time.NewTimer(_retryInterval)
	defer timer.Stop()
	defer c.wg.Done()
	for {
		select {
		case <-timer.C:
			c.pumpUpdates()
			timer.Reset(_retryInterval)
		// TODO
		case <-c.ctx.Done():
			return
		}
	}
}

func (c *consulClient) preload() error {
	if err := c.cache.loadFile(c.getDumpFileName()); err != nil {
		return err
	}
	return nil
}

func (c *consulClient) initConsulIndex() {
	var lastConsulIndex uint64
	c.cache.kv.Range(func(key, value interface{}) bool {
		index, _ := strconv.ParseUint(value.(string), 10, 64)
		if index > lastConsulIndex {
			lastConsulIndex = index
		}
		return true
	})
	c.setConsulIndex(lastConsulIndex)
	c.setLastConsulIndex(lastConsulIndex)
}

func (c *consulClient) pumpUpdates() (err error) {
	var url = c.makeUrl(_defaultConfApi, strconv.FormatUint(c.consulIndex, 10))
	var (
		rb    []byte
		index string
	)
	fmt.Println("consul watch url:", url)
	if rb, index, err = c.request(url); err != nil {
		return
	}
	uIndex, _ := strconv.ParseUint(index, 10, 64)
	c.setConsulIndex(uIndex)
	if err = c.handleUpdate(rb, err); err != nil {
		return
	}
	c.setLastConsulIndex(uIndex)
	return
}

func (c *consulClient) handleUpdate(rb []byte, perr error) error {
	if perr == ErrStatusNotFound { // delete Config

	}

	if perr == ErrStatusGatewayTimeout {
		return nil
	}

	if perr != nil { // need delete conf path
		return nil
	}

	// handle modify/new
	var configList []*configEntity
	var err error
	if err := json.Unmarshal(rb, &configList); err != nil {
		err = pkgerrors.Wrap(err, fmt.Sprintf("Unmarshal body error,body: \"%s\"", string(rb)))
		return err
	}
	var result = map[string]*configEntity{}
	for _, v := range configList {
		result[v.Key] = v
	}
	kv := c.cache.dump()
	for k, _ := range kv {
		elem, ok := result[k]
		if !ok { // need delete
			filename := c.getConfigFileName(k)
			if err = os.Remove(filename); err != nil {
				return pkgerrors.Wrap(err, fmt.Sprintf("remove file=%s error", filename))
			}
			fmt.Println(fmt.Sprintf("delete key=%s", k))
			c.cache.kv.Delete(k)
			continue
		}

		if elem.ModifyIndex > c.lastConsulIndex { //modify
			fmt.Println(fmt.Sprintf("modify key=%s", elem.Key))
			if err = c.reloadFile(elem); err != nil {
				return err
			}
			c.cache.kv.Store(elem.Key, strconv.FormatUint(elem.ModifyIndex, 10))
		}
	}

	for k, elem := range result {
		if _, ok := kv[k]; !ok { // need add
			fmt.Println(fmt.Sprintf("add key=%s", elem.Key))
			if err = c.reloadFile(elem); err != nil {
				return err
			}
			c.cache.kv.Store(elem.Key, strconv.FormatUint(elem.ModifyIndex, 10))
		}
	}

	return c.cache.dumpFile(c.getDumpFileName())
}

func (c *consulClient) setConsulIndex(index uint64) {
	c.consulIndex = index
}

func (c *consulClient) setLastConsulIndex(index uint64) {
	c.lastConsulIndex = index
}

func (c *consulClient) request(url string) (rb []byte, index string, err error) {
	var (
		req  *http.Request
		resp *http.Response
	)

	if req, err = http.NewRequest(http.MethodGet, url, strings.NewReader("")); err != nil {
		err = pkgerrors.Wrap(err, "http.NewRequest err")
		return
	}

	if resp, err = c.httpCli.Do(req); err != nil {
		err = pkgerrors.Wrap(err, fmt.Sprintf("request url \"%s\" err", url))
		return
	}

	defer resp.Body.Close()
	if resp.StatusCode == http.StatusOK {
		if rb, err = ioutil.ReadAll(resp.Body); err != nil {
			return
		}
		index = resp.Header.Get(_consulIndexHeaderKey)
		return
	} else {
		switch resp.StatusCode {
		case http.StatusNotFound:
			return nil, "", ErrStatusNotFound
		case http.StatusGatewayTimeout:
			return nil, "", ErrStatusGatewayTimeout
		default:
			return nil, "", ErrStatusNotOk
		}
	}
}

// reloadFile parse configEntity and flush to file
func (c *consulClient) reloadFile(elem *configEntity) (err error) {
	var contents []byte
	filename := c.getConfigFileName(elem.Key)
	if contents, err = c.getConfigValue(elem); err != nil {
		return err
	}
	return c.putContents(filename, contents)
}

// getConfigFileName get real config file name
func (c *consulClient) getConfigFileName(key string) string {
	return fmt.Sprintf("%s/%s", c.makeRealConfPath(), key[len(fmt.Sprintf("%s/", c.makeRealWatchPrefix())):])
}

// getConfigValue return base64 decode configEntity.Value
func (c *consulClient) getConfigValue(elem *configEntity) ([]byte, error) {
	var (
		contents []byte
		err      error
	)
	if contents, err = base64.StdEncoding.DecodeString(elem.Value); err != nil {
		err = pkgerrors.Wrap(err, fmt.Sprintf("base64 decode str=%s error", elem.Value))
		return nil, err
	}
	return contents, err
}

func (c *consulClient) getDumpFileName() string {
	filename := "encode.data"
	return path.Join(c.conf.TempConfPath, filename)
}

func (c *consulClient) putContents(filename string, contents []byte) (err error) {
	tmpFilename := fmt.Sprintf("%s/%s", c.conf.TempConfPath, uuid.New().String())
	defer os.Remove(tmpFilename)
	if err = checkOrMkdir(path.Dir(filename)); err != nil {
		return err
	}
	if err = ioutil.WriteFile(tmpFilename, contents, os.ModePerm); err != nil {
		return pkgerrors.Wrap(err, fmt.Sprintf("putContent filename=%s error", filename))
	}

	if err = os.Rename(tmpFilename, filename); err != nil {
		return pkgerrors.Wrap(err, fmt.Sprintf("rename oldpath=%s, newpath=%s  error", tmpFilename, filename))
	}
	return nil
}

func (c *consulClient) configConfPath() error {
	if err := checkOrMkdir(c.makeRealConfPath()); err != nil {
		return err
	}
	if err := checkOrMkdir(c.conf.TempConfPath); err != nil {
		return err
	}
	return nil
}

func checkOrMkdir(path string) (err error) {
	if _, err = os.Stat(path); err == nil {
		return
	}

	if os.IsNotExist(err) {
		if err = os.MkdirAll(path, os.ModePerm); err != nil {
			err = pkgerrors.Wrap(err, fmt.Sprintf("mkdir path=%s fail", path))
			return
		}
	}

	if err != nil {
		err = pkgerrors.Wrap(err, fmt.Sprintf("Stat path=%s fail", path))
		return
	}
	return
}

func buildConfigForConsul() (conf *Config, err error) {
	if os.Getenv("CONSUL_PATH") != "" {
		confPath = os.Getenv("CONSUL_PATH")
	}
	if os.Getenv("CONSUL_TEMP_PATH") != "" {
		tempConfPath = os.Getenv("CONSUL_TEMP_PATH")
	}
	if confPath == "" {
		err = errors.New("no consul path")
		return
	}

	if tempConfPath == "" {
		err = errors.New("no consul temp path")
		return
	}

	conf = &Config{
		Host:         confHost,
		Port:         confPort,
		Env:          confEnv,
		ConfPath:     strings.TrimRight(confPath, "/"),
		WatchPrefix:  confWatchPrefix,
		TempConfPath: strings.TrimRight(tempConfPath, "/"),
	}
	return conf, nil
}

func (c *consulClient) makeUrl(api string, index string) (query string) {
	// host/v1/kv/data/test?index=index
	return fmt.Sprintf(api, c.conf.Host, c.conf.Port, c.makeRealWatchPrefix(), index)
}

func (c *consulClient) makeRealWatchPrefix() (watchPrefix string) {
	return fmt.Sprintf("%s/%s", strings.TrimRight(c.conf.WatchPrefix, "/"), c.conf.Env)
}

func (c *consulClient) makeRealConfPath() (confPath string) {
	return fmt.Sprintf("%s/%s", strings.TrimRight(c.conf.ConfPath, "/"), c.conf.Env)
}
